package ngat.oss;

import ngat.phase2.*;
import ngat.phase2.nonpersist.*;
import ngat.util.*;
import ngat.util.logging.*;
import ngat.astrometry.*;
import ngat.instrument.*;
import ngat.message.OSS.*;

import jyd.dbms.*;
import jyd.storable.*;
import jyd.collection.*;

import java.io.*;
import java.util.*;
import java.text.*;

public class TestScheduler implements Scheduler {

    /** How long we will wait for a fixed group if no flexibles available.*/
    protected static long MAX_IDLE = 180000L;

    /** Small angle - used to avoid div-by-zero errors.*/
    public static final double VERY_SMALL = Math.toRadians(0.1);

    /** Small number - used to avoid div-by-zero errors.*/
    public static final double SMALL_NUMBER = 0.001;

    /** Angle at which we consider sun is up = end-of-obs.*/
    public static final double SUNRISE_ANGLE = Math.toRadians(-9.0);

    /** Root entry to DB.*/
    private Root root;

    /** Current TAG.*/
    private Tag tag;

    /** Curent User.*/
    private User user;
    
    /** Current Proposal.*/
    private Proposal proposal;

    /** Current Group.*/
    private Group group;

    /** Currently processing descriptor.*/
    private ScheduleDescriptor sd;
 
    /** ISO 8601 Dateformat.*/
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 'T' HH:mm:ss z");
    private SimpleDateFormat udf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    private SimpleTimeZone UTC = new SimpleTimeZone(0, "UTC");

    /** Logger.*/
    private Logger logger;

    /** Used to calculate a Group's slew time.*/
    long groupSlewTime = 0L;

    /** Used to calulate processing time.*/
    long procTime = 0L;

    /** Records start of processing.*/
    long start = 0L;

    /** Cache the current telescope altitude (rads).*/
    double telescopeAltitude;

    /** Cache the current telescope azimuth (rads).*/
    double telescopeAzimuth;

    /** Cache the current azimuth negative wrap (rads).*/
    double telescopeAzNegLimit; 

    /** Cache the current azimuth positive wrap (rads).*/
    double telescopeAzPosLimit;

    /** Cache the current rotator negative wrap (rads).*/
    double telescopeRotNegLimit; 

    /** Cache the current rotator positive wrap (rads).*/
    double telescopeRotPosLimit;

    /** Cache the current altitude low limit (rads).*/
    double telescopeAltLowLimit; 

    /** Cache the current altitude high limit (rads).*/
    double telescopeAltHighLimit;

    /** Cache telescope slew az rate (rad/sec.)*/
    double azRate;
    
    /** Cache telescope slew alt rate (rad/sec.)*/
    double altRate;
    
    /** True if the AG is available.*/
    boolean autoguiderAvailable;

    /** True if the DB's rotator settings will be ignored by the RCS.*/
    boolean rotatorOverridden = false;

    /** Current seeing.Will be one of: Group. POOR, AVERAGE, EXCELLENT. NOT quite the same as seeing.*/
    int currentSeeing;

    /** Current corrected seeing for zenith Rband.*/
    double correctedSeeing;

    /** Current photometricity.*/
    boolean currentPhotom;

    /** How is photom generated.*/
    String currentPhotomSrc;

    /** Transit elevation weight.*/
    double wgtTransit;

    /** Condition weighting factor.*/
    double wgtSeeing;

    /** Lunar weighting factor.*/
    double wgtLunar;

    /** Time allocation scale factor.*/
    double scaleAlloc;

    /** Completion scale factor.*/
    double scaleComp;

    /** Fairness weighting scale factor.*/
    double scaleFairness;

    /** Group priority boost weighting.*/
    double wgtPriority;

    /** Ephemeris missed cycles weighting.*/
    double wgtEphem;

    /** Lateness weighting.*/ 
    double wgtLateness;

    /** Repeat group missed interval weighting.*/
    double wgtMissedTime;

    /** Cond time usage weight.*/
    double wgtAlloc;

    /** Completion weight.*/
    double wgtComp;

    /** Solar Elevation which is considered to indicate onset of dark twilight (rads).*/
    double darkTwilightElevation;

    /** Solar Elevation which is considered to indicate onset of bright twilight (rads).*/
    double brightTwilightElevation;

    /** Solar Elevation which is considered to indicate onset of full night (rads).*/
    double nightElevation;

    /** Factor (F) to use to multiply nominal (or maximal) group exec time for 
     * calculating time-above-horizon veto.
     * i.e. if ( F * nominal > available time then veto, where F >= 1.0)
     */
    double executionTimeVetoFactor;

    /** Factor to add to nominal exce time as a fixed cost.*/
    double executionTimeFixedFactor;

    /** Minimum target distance from moon at which sky is considered BRIGHT (rads).*/
    double darknessCriterionLunarDistance;

    /** Minimum target distance from moon to use for bright requirment targets. (rads).*/
    double absoluteMinimumLunarDistance;

    /** Buffer time prior to FixedGroup by when an executing group must be completed.(millis).*/
    long fixedGroupBufferTime;

    /** Minimum gap within which a FixedGroup will be started.*/
    long fixedGroupMinGapTime;

    /** Dome lower limit (rads).*/
    double domeLimit;

    /** Altitude high limit as temporary feature.*/
    double altTempHighLimit;

    /** Zenith Exclusion zone size (rads).*/
    double zezAngle;

    /** Offset before sunrise when we expect to stop observing (ms).*/
    long sunriseOffset;

    /** Maximum idle time before starting a fixed group (ms).*/
    long maxIdle;

    /** Cache lunar position.*/
    Position moon;

    /** Cache solar position.*/
    Position sun;

    /** Time till sunrise (ms).*/
    long ttsunrise;

    /** Time till sunrise above twilight angle (ms).*/
    long ttday;

    /** Owner of proposal lock who we can ignore.*/
    String ignoreLockOwner = Scheduler.RCS_LOCK;

    SortedSet candidates;

    List background;

    /** ID for Erics proposal.*/
    String ericsProposal;

    double skewbias = 0.0;

    /** Sweep counter.*/
    private volatile int sweepCount = 0;


    /** Create a TestScheduler.
     * Initially uses logger SCHEDULE at level 1.*/
    public TestScheduler(Root root) {
	this.root = root;

	logger = LogManager.getLogger("SCHEDULE");
	logger.setLogLevel(1);

	sdf.setTimeZone(UTC);
	udf.setTimeZone(UTC);

	candidates = new TreeSet(new ScheduleItemComparator());
	background = new Vector();

    }

    /** Load the current conditions from file.*/
    public void init(ConfigurationProperties params) {

	ignoreLockOwner = params.getProperty("lock.owner", Scheduler.RCS_LOCK); // may be null.

	maxIdle = params.getLongValue("max.idle", MAX_IDLE);

	String strSeeing = null;
	String strPhotom = null;
	String strSrc    = null;
	String strDate   = null;
	Properties atmos = new Properties();

	try {
	    FileInputStream cin = new FileInputStream("/occ/tmp/.conditions");
	   
	    atmos.load(cin);

	    // decide where photom is coming from...
	    strSrc    = atmos.getProperty("source");
	    strPhotom = atmos.getProperty("extinction", "SPECTROSCOPIC");
	    strDate   = atmos.getProperty("updated");

	    if (strPhotom.equals("PHOTOMETRIC"))
		currentPhotom = true;
	    else
		currentPhotom = false;

	    currentPhotomSrc = strSrc;
	    
	} catch (Exception e) {
	    System.err.println("Could not read conditions - assumed POOR and Non-Photometric: "+e);	 
	    currentPhotom = false;
	    return;
	}


	// here is where the manual seeing data must be entered
	
	long uago = 0L;
	try {
	    long updated = (udf.parse(strDate)).getTime();
	    uago = System.currentTimeMillis() - updated;
	} catch (Exception ux) {
	    System.err.println("Error parsing photom update time:");
	}
	System.err.println("Extinction conditions: "+strPhotom+" set "+(uago/1000)+"s ago by "+strSrc+" source");
	
    }


    /** Just calls configure(ConfigurationProperies) after opening the File.*/
    public void configure(File file) throws IOException, IllegalArgumentException {
	ConfigurationProperties props = new ConfigurationProperties();
	props.load(new FileInputStream(file));
	configure(props);
    }
    
    /** Setup configurable parameters.*/
    public void configure(ConfigurationProperties config) throws IllegalArgumentException {
	//
	// Factor (F) to use to multiply nominal (or maximal) group exec time for 
	// calculating time-above-horizon veto.
	// i.e. if ( F * nominal > available time then veto, where F >= 1.0)
	executionTimeVetoFactor = config.getDoubleValue("execution.time.veto.factor", 1.5);

	// Fixed cost to add to nominal exec time (3Mins).
	executionTimeFixedFactor = config.getDoubleValue("execution.time.fixed.factor", 3*60*1000.0);

	// rotator is overridden - this is a special fudge to handle the case where the rotator
	// settings in the DB are ignored by the RCS and set to a generic (MOU0+FLT) - any
	// rotator requirments for MOUx are then ignored as they may be outside the available
	// 'safe' range which is not known to the system as such
	rotatorOverridden = (config.getProperty("rotator.override") != null);
 
	//
	// Some option relating to the skew of the Monitor window weighting
	// i.e. do we weight all time in a window equally or skew towards start, middle, end.
	//

	// Whether to use the Group's score with priority included or to use the score
	// of the highest scoring group in a proposal for the priority group in that proposal.
	//
       
	domeLimit=  Math.toRadians(config.getDoubleValue("dome.limit", 20.0));
	altTempHighLimit =  Math.toRadians(config.getDoubleValue("temp.altitude.ceiling", 90.0));
		
	// The elevation of the sun for full night determination (degs).
	nightElevation          = Math.toRadians(config.getDoubleValue("night.elevation", -12.0));
		
	// The elevation of the sun for dark twilight determination (degs).
	darkTwilightElevation   = Math.toRadians(config.getDoubleValue("dark.twilight.elevation", -6.0));

	// The elevation of the sun for bright twilight determination (degs).
	brightTwilightElevation = Math.toRadians(config.getDoubleValue("bright.twilight.elevation", -3.0));
				 
	// Size of Zenith Exclusion Zone (degs).
	zezAngle = Math.toRadians(config.getDoubleValue("zenith.exclusion.zone", 1.0));

	// Time offset before sunrise when observing is expected to complete by.
	sunriseOffset = config.getLongValue("sunrise.stop.offset", 30*60*1000L);

	// How close do we let the moon get before we exclude Dark obs (degs).
	darknessCriterionLunarDistance = Math.toRadians(config.getDoubleValue("darkness.criterion.lunar.distance", 40.0));

	// How close do we let the moon get ever (even for Bright obs) (degs).
	absoluteMinimumLunarDistance = Math.toRadians(config.getDoubleValue("absolute.minimum.lunar.distance", 15.0));

	// How much time before an FG will we try to squeeze other obs into.
	fixedGroupBufferTime = config.getLongValue("fixed.group.buffer.time", 5*60*1000L);

	// How soon we will consider doing a FixedGroup - if ttfg is less than this we dont even try.
	fixedGroupMinGapTime = config.getLongValue("fixed.group.min.gap.time", 1*60*1000L);

	// Tramsit weight.
	wgtTransit = config.getDoubleValue("transit.weight", 0.5);

	// Weights for condition-matching functions.
	wgtSeeing = config.getDoubleValue("seeing.condition.matching.weight", 0.25);
	wgtLunar  = config.getDoubleValue("lunar.condition.matching.weight",  0.25);

	// Weighting to multiply group-priority booster by.
	wgtPriority = config.getDoubleValue("priority.weight", 0.1);
	
	// Time allocation weighting falloff factor
	scaleAlloc = config.getDoubleValue("allocation.scale", 5.0);

	// Completion allocation weighting falloff factor
	scaleComp = config.getDoubleValue("completion.scale", 5.0);

	// Time distribution fairness weighting falloff factor.
	scaleFairness = config.getDoubleValue("fairness.scale", 2.0);

	// Ephemeris group missed cycle weighting.
	wgtEphem = config.getDoubleValue("ephem.weight", 0.5);

	// Lateness (how far (fraction) into its active period) weighting.
	wgtLateness = config.getDoubleValue("lateness.weight", 1.0);

	// Missed intervals by Interval (repeating) group.
	wgtMissedTime = config.getDoubleValue("missed.interval.weight", 0.3);

	// Allocation usage frac weight.
	wgtAlloc = config.getDoubleValue("allocation.weight", 0.3);
	
	// Completion score weight.
	wgtComp = config.getDoubleValue("completion.weight", 0.3);


	ericsProposal = config.getProperty("erics.proposal");

	// bias for test purposes
	//try {
	//  skewbias = 1/Double.parseDouble(System.getProperty("skew"));
        //} catch (Exception e) {}
	
    }
    
    /** No special action here.*/
    public void dispose(){}

    
    /** Gets the next FixedGroup (for time constraint) this is generic to all
     * schedulers so uses the convenience method from Scheduling.
     *
     * This method will be replaced by a pre-schedule call to cache and sort
     * the list of available FixedGroups in the DB at start of SCI_OPS.
     *
     */
    public FixedGroup getNextFixedGroup() {return Scheduling.getNextFixedGroup(root, fixedGroupMinGapTime); }
    
    
    /**
     * Return the next and best Group to execute.
     * 
     * (This call should contain some constraint information from the RCS 
     * - at the least:-
     *
     * <ul>
     *  <li> startTime : The time from which the Group should be able to start.
     * 
     *  <li> endTime   : The time by which the Group must be completely done.
     * </ul>
     * 
     * E.g. getBestGroup(long startAfter, long finishBefore);
     */
    public ScheduleDescriptor getBestGroup() {

	sweepCount++;
	long now = System.currentTimeMillis();

	DefaultScheduleMonitor dsm = Oss.getInstance().getScheduleMonitor();
	if (dsm != null) {
	    try {
		dsm.notifyListenersSweepStarted(now, sweepCount);
	    } catch (Exception e) {
		e.printStackTrace();
	    }
	}

	ChargingModel chargeModel = Oss.getInstance().getChargingModel();
	
	ScheduleDescriptor best = new ScheduleDescriptor(null, -100000.0, 0L);

	candidates.clear();
	background.clear();
	
	// Cache these, they can't be updated while this is running due to Transaction.
	telescopeAltitude     = TelescopeStatus.getTelescopeAltitude();
	telescopeAzimuth      = TelescopeStatus.getTelescopeAzimuth();

	telescopeAzNegLimit   = TelescopeStatus.getAzNegLimit();
	telescopeAzPosLimit   = TelescopeStatus.getAzPosLimit();	

	telescopeAltLowLimit  = TelescopeStatus.getAltLowLimit();
	telescopeAltHighLimit = TelescopeStatus.getAltHighLimit();

	telescopeRotNegLimit  = TelescopeStatus.getRotNegLimit();
	telescopeRotPosLimit  = TelescopeStatus.getRotPosLimit();
	
	TelescopeParameters telescopeParameters = ServerContext.getTelescopeParameters();
	azRate  = telescopeParameters.getTelescopeAzimuthSlewRate();
	altRate = telescopeParameters.getTelescopeAltitudeSlewRate();

	autoguiderAvailable = TelescopeStatus.isAutoguiderAvailable();
	
	// Cached for scheduling run.
	moon = Scheduling.getLunarPosition();
	sun  = Scheduling.getSolarPosition();

	// Work out sunrise i.e. ms from NOW
	ttsunrise = sun.getRiseTimeToday(0.0) - now;	
	if (ttsunrise < 0L)
		ttsunrise += 86400*1000L;

	ttday = sun.getRiseTimeToday(SUNRISE_ANGLE) - now;
	if (ttday < 0L)
		ttday += 86400*1000L;

	int predictedSeeing = Scheduling.getSeeingPrediction();
	
	currentSeeing = predictedSeeing;	
	
	correctedSeeing = Scheduling.getCorrectedSeeingValue();

	String strCond = "Unknown";
	switch (currentSeeing) {
	case Group.POOR:
	    strCond = "POOR ( 1.3\" <= seeing < 3.0\")";
	    break;
	case Group.AVERAGE:
	    strCond = "AVERAGE ( 0.8\" <= seeing < 1.3\")";
	    break;
	case Group.EXCELLENT:
	    strCond = "EXCELLENT ( seeing < 0.8\")";
	    break;
	case Group.CRAP:
	    strCond = "AWFUL (seeing >= 3.0\")";
	    break;
	default:
	    strCond = "Default AWFUL ( seeing >= 3.0\")";
	}

	// START new fg stuff.

	FixedGroup nfg = Scheduling.findNextFixedGroup(now, fixedGroupMinGapTime);

	String nfginfo = null;
	if (nfg != null) {	
	    boolean cando = Scheduling.checkValid(nfg);
	    if (cando) {
		nfginfo = "Next FixedGroup: "+nfg.getFullPath()+
		    "\n      Due in:        "+((nfg.getFixedTime()-now)/1000.0)+" secs"+ 
		    "\n      At:            "+sdf.format(new Date(nfg.getFixedTime()))+
		    "\n      Buffer time:   "+(fixedGroupBufferTime/1000.0)+" secs"+
		    "\n      Setup time:    "+(fixedGroupMinGapTime/1000.0)+" secs"+
		    "\n                     is executable";
	    } else {
		nfginfo = "Next FixedGroup: "+nfg.getFullPath()+
		    "\n      Due in:        "+((nfg.getFixedTime()-now)/1000.0)+" secs"+
		    "\n      At:            "+sdf.format(new Date(nfg.getFixedTime()))+
		    "\n                     is NOT executable for some reason";
	    }
	    //Scheduling.nextFGroupTime = nfg.getFixedTime();
	    //Scheduling.setNextFGroup(nfg); // This sets its time also. should have been set anyway..
	    
	} else {
	    nfginfo = "FixedGroup:      There are NO executable FixedGroups due)";
	}
	// END new fg stuff
	
	
// 	String nfginfo = null;
// 	if (Scheduling.getNextFGroupTime() < now)
// 	    nfginfo = "FixedGroup due at:  (There are no FixedGroups due)";
// 	else
// 	    nfginfo = "FixedGroup due at: "+sdf.format(new Date(Scheduling.getNextFGroupTime()))+
// 		"\n      Time until:    "+((Scheduling.getNextFGroupTime()-now)/1000.0)+" Secs"+ 
// 		"\n      Buffer Time:   "+(fixedGroupBufferTime/1000.0)+" Secs"+
// 		"\n      Min. Gap:      "+(fixedGroupMinGapTime/1000.0)+" Secs";
	
	boolean badZone = (telescopeAzPosLimit < 2*Math.PI + telescopeAzNegLimit);

	mylog(sweepCount, "START_SWEEP "+sweepCount);
	
	logger.log(1,"Start of scheduling: "+		   
		   "\nCurrent Telescope location is: "+
		   "\n    Azimuth:    "+Position.toDegrees(telescopeAzimuth, 3)+		
		   "\n     Limits:    "+Position.toDegrees(telescopeAzNegLimit, 2)+
		                      " - "+Position.toDegrees(telescopeAzPosLimit, 2)+")"+
		   "\n   Bad zone:   ["+(badZone ? Position.toDegrees(telescopeAzPosLimit,2)+", "+
					   Position.toDegrees(2*Math.PI + telescopeAzNegLimit,2) : "NONE") +"]"+

		   "\n    Altitude:   "+Position.toDegrees(telescopeAltitude, 3)+
		   "\n     Limits:    "+Position.toDegrees(telescopeAltLowLimit, 2)+
		                      " - "+Position.toDegrees(telescopeAltHighLimit, 2)+")"+
		   "\n    Ceiling:      "+Position.toDegrees(altTempHighLimit,2)+
		   
		   "\n Rotator Limits:"+Position.toDegrees(telescopeRotNegLimit, 3)+
		   " - "+Position.toDegrees(telescopeRotPosLimit, 3)+")"+
		   
		   "\n Autoguider:     "+(autoguiderAvailable ? "AVAILABLE" : "NOT AVAILABLE")+
		   
		   "\n Slew Rates:     "+
		                       Math.toDegrees(azRate)+ " deg/sec AZ, "+
		                       Math.toDegrees(altRate)+" deg/sec ALT"+

		   "\n Predicted Seeing:   "+currentSeeing+" : "+strCond+" using Zenith R-band: "+correctedSeeing+
		   "\n Extinction "+currentPhotomSrc+" : "+(currentPhotom ? "PHOTOMETRIC" : "SPECTROSCOPIC")+
		   "\n Alt Low Limit:      "+Position.toDegrees(domeLimit, 1)+
		   "\n Alt ZEZ limit:      "+Position.toDegrees(zezAngle,2)+" deg"+  
		   
		   "\n Bright-Rise angle:  "+Position.toDegrees(SUNRISE_ANGLE,2)+" degs"+
		   "\n MLD (Bright time):  "+Position.toDegrees(absoluteMinimumLunarDistance, 1)+
		   "\n     (Dark   time):  "+Position.toDegrees(darknessCriterionLunarDistance, 1)+
		   "\n Zoning info:-        "+
		   "\n   B-twi elevation:  "+Position.toDegrees(brightTwilightElevation,2)+
		   "\n   D-twi elevation:  "+Position.toDegrees(darkTwilightElevation,2)+
		   "\n   Night elevation   "+Position.toDegrees(nightElevation,2)+
		   "\n ExecutionModel:-"+
		   "\n   Mult factor:      "+executionTimeVetoFactor+
		   "\n   Fixed factor:     "+executionTimeFixedFactor+
		   "\n ChargeModel:-"+
		   "\n   Slew rate:         "+ExecutionCosting.slewRate+" per slew"+    
		   "\n   Offset rate:       "+ExecutionCosting.offsetRate+" per move"+
		   "\n   Config rate:       "+ExecutionCosting.configRate+" per reconfig"+
		   "\n   Readout rate:      "+ExecutionCosting.readoutRate+" per exposure"+
		   "\n "+nfginfo+		  
		   "\n External Time constraints:-"+
		   "\n   Delta Time:        "+(Scheduling.getDeltaTime()/1000.0)+" secs"+
		   "\n   Stop observing:    "+(sunriseOffset/60000L)+" mins before sunrise"+
		   "\n   Next STConstraint: "+sdf.format(new Date(Scheduling.getNextTimeConstraint()))+
		   "\n      Time Delay:     "+((Scheduling.getNextTimeConstraint() - now)/1000.0)+" secs.");
	
	logger.log(1, "Sun and moon:-"+
		   "\n   Sun:          "+sun+
		   "\n     Elev:       "+Position.toDegrees(sun.getAltitude(), 3)+
		   "\n     Az:         "+Position.toDegrees(sun.getAzimuth(), 3)+
		   "\n     Rise in:    "+(ttsunrise/1000)+" secs"+
		   "\n     B-rise in:  "+(ttday/1000)+" secs"+
		   "\n   Moon:         "+moon+
		   "\n     Elev:       "+Position.toDegrees(moon.getAltitude(), 3)+
		   "\n     Az:         "+Position.toDegrees(moon.getAzimuth(), 3));
	
	int count = 0;
	int bcount = 0;
	
	// Walk the Phase2 tree.
	Iterator iTag = root.listAllTags();
	while (iTag.hasNext()) {
	    
	    tag = (Tag)iTag.next();
	    
	    // Exclude certian TAGs here.
	    // if ((! tag.getName().equals("JMU")) &&
	    //(! tag.getName().equals("UK-PUST"))) continue;
	    
	    Iterator iUser = tag.listAllUsers();
	    while (iUser.hasNext()) {
		
		user = (User)iUser.next();
		
		Iterator iProp = user.listAllProposals();
		while (iProp.hasNext()) {
		    
		    KVPair pair = (KVPair)iProp.next();
		    String name = (String)pair.getKey();
		    Path   path = new Path(user.getFullPath(), name);
		    
		    // Extract the Proposal, it may be memory cached by OSS_Util.
		
		    System.err.println("Looking for Proposal: "+path);

		    try {
			proposal = OSS_Utilities.fetchProposal(path);
			
			if (proposal == null) continue;

			// System.err.println("Scheduling Proposal: "+proposal.getFullPath()+
			//"\n\tActivated: "+sdf.format(new Date(proposal.getActivationDate()))+
			//"\n\tExpires:   "+sdf.format(new Date(proposal.getExpiryDate())));
			
			logger.log(3, "Scheduler", "Checking Proposal: "+path);
			
			// Outside activation period.
			logger.log(3, "Scheduler", "Checking activation period...");
			if (proposal.getActivationDate() > now ||
			    proposal.getExpiryDate() < now) {

			    mytest(sweepCount, "PROPOSAL", name,
				  "Outside activation period: "+
				  sdf.format(new Date(proposal.getActivationDate()))+" - "+
				  sdf.format(new Date(proposal.getExpiryDate())));	
			    
			    logger.log(1, "Scheduler", "Proposal outside activation period: "+
				       sdf.format(new Date(proposal.getActivationDate()))+" - "+
				       sdf.format(new Date(proposal.getExpiryDate())));			
			    continue;
			}		    
			
			// No time left.
			logger.log(3, "Scheduler", "Checking for time left...");
			if (proposal.getUsedTime() >= proposal.getAllocatedTime()) {
			    
			    mytest(sweepCount, "PROPOSAL", name,
				  "Out of time: Used: "+proposal.getUsedTime()+
				  " of "+proposal.getAllocatedTime());
			    
			    logger.log(1, "Scheduler", "Proposal out of time: Used: "+proposal.getUsedTime()+
				       " of "+proposal.getAllocatedTime());
			    continue;
			}
			
			// Being edited or RCS Crashlock.
			logger.log(3, "Scheduler", "Checking lock status...");
			if (proposal.isLocked()) {
			    if (proposal.getLastEditor().equals(ignoreLockOwner)) {
				logger.log(1, "Scheduler", 
					   "Proposal was locked by "+ignoreLockOwner+" Auto-unlocking");
				proposal.forceUnLock();
				proposal.setLastUnLocked(now);
			    
				try {
				    OSS_Utilities.storeProposal(proposal);
				} catch (IOException ix) {
				    logger.log(1, "Scheduler",
					       "** Warning- Unable to store auto-unlocked proposal:"+ix);
				}
				
			    } else {
				mytest(sweepCount, "PROPOSAL", name,
				      "Locked using key "+proposal.getLock()+
				      " By "+proposal.getLastEditor()+
				      " On "+sdf.format(new Date(proposal.getLastLocked())));
				
				logger.log(1, "Scheduler", 
					   "Proposal was locked using key "+proposal.getLock()+
					   " By "+proposal.getLastEditor()+
					   " On "+sdf.format(new Date(proposal.getLastLocked())));
				continue;
			    }
			}
			
			Iterator iGroup = proposal.listAllGroups();
			logger.log(3, "Scheduler", "Checking groups...");
			int gc = 0;
			while (iGroup.hasNext()) {
			    
			    group = (Group)iGroup.next();
			    
			    logger.log(3, "Scheduler", "Checking group "+proposal.getName()+"/"+group.getName());
			    gc++;

			    // PEST or Admin lock.
			    logger.log(3, "Scheduler", "Checking group lock status...");
			    if (group.isLocked()) {

				mytest(sweepCount, "TEST_GROUP", proposal.getName()+"/"+group.getName(),
				       "FAIL: Locked");
				
				logger.log(1, "Scheduler", "Group "+group.getFullPath()+" is locked");
				continue;
			    }
			    
			    logger.log(3, "Scheduler", "Checking group tabu status...");
			    if (Scheduling.isTabu(group.getFullPath(), now)) {

				mytest(sweepCount, "TEST_GROUP", proposal.getName()+"/"+group.getName(),
				       "FAIL: On tabu list until "+
				       sdf.format(new Date(Scheduling.getTabuTime(group.getFullPath()))));
				
				logger.log(1, "Scheduler", "Group "+group.getFullPath()+
					   " is on Taboo list until: "+
					   sdf.format(new Date(Scheduling.getTabuTime(group.getFullPath()))));
				continue;
			    }
			    
			    // We dont do these here, getNextFixedGroup() will extract.
			    logger.log(3, "Scheduler", "Checking group timing class for "+group.getClass().getName()+"...");
			    
			    if (group instanceof FixedGroup) 
				
				// ------------
				// FIXED_GROUP.
				// ------------
			
				continue;
			    
			    else if (group instanceof MonitorGroup)
				
				// --------------
				// MONITOR_GROUP.
				// --------------
				
				sd = doScheduleMonitorGroup((MonitorGroup)group, proposal, tag);
			    
			    else if (group instanceof EphemerisGroup)
				
				// ----------------
				// EPHEMERIS_GROUP.
				// ----------------
				
				sd = doScheduleEphemerisGroup((EphemerisGroup)group, proposal, tag);
			    
			    else if (group instanceof RepeatableGroup)
				
				// -----------------
				// REPEATABLE_GROUP.
				// -----------------
				
				sd = doScheduleRepeatableGroup((RepeatableGroup)group, proposal, tag);

			    else
				
				// --------------
				// GENERIC GROUP.
				// --------------
				sd = doScheduleFlexibleGroup(group, proposal, tag);
			    
			    if (sd == null)
				continue;
			    
			    // If its got through then check against best-so-far.
			    if (sd.getAllowed()) {

				// ### XXX if its an Eric we dont set it as BestGroup but 
				// ### XXX it will still be added to candidates

				//if (ericsProposal.equals(proposal.getFullPath())) {
				// System.err.println("Skipping Eric proposal...");
				//}
				
				if (sd.getScore() > best.getScore()) {
				    best.setGroup(group);
				    best.setAllowed(true);
				    best.setScore(sd.getScore());
				    best.setNominalTime((long)group.getNominalExecutionTime());
				    best.setExecTime((long)(executionTimeVetoFactor*group.getNominalExecutionTime()));	
				    best.clearMetrics();
				    Iterator is = sd.listMetricKeys();
				    while (is.hasNext()) {
					String key = (String)is.next();
					best.addMetric(key, sd.getMetric(key));
				    }
				}

				mytest(sweepCount, "TEST_GROUP", proposal.getName()+"/"+group.getName(),
				       "OKAY");

				logger.log(1, "Scheduler", 
					   "Group confirmed OKAY: "+sd);
				
				// Add to candidates or background groups.
				if (group.getPriority() == -2) {
				    background.add(sd); 
				    bcount++;	
				} else { 
				    //candidates.add(group.getFullPath());
				    candidates.add(sd);
				    count++;
				    logger.log(1, "Scheduler", 
					       "Adding to candidate list: "+group.getFullPath()+", so far "+
					       count);
				}
			    } else {

				mytest(sweepCount, "TEST_GROUP", group.getName(),
				       "FAIL: "+sd.getFailureReason());

				logger.log(1, "Scheduler", 
					   "Group FAIL: "+group.getFullPath()+" : "+sd.getFailureReason());
			    }
			} // Groups.
			
			if (gc == 0) {
			    mytest(sweepCount, "TEST_PROPOSAL", name,
				   "FAIL: No groups");

			    logger.log(1, "Scheduler",
				       "Proposal contains no groups.");
			}
			
		    } catch (IOException iox) {
			System.err.println("Proposal: "+path.toString()+
					   " IOError fetching from Database: "+iox);	
			logger.dumpStack(1, iox);
			
		    } catch (Exception e) {
			
			System.err.println("Proposal: "+path.toString()+
					   " Error fetching from Database: "+e);	
			logger.dumpStack(1, e);
			
		    }	
		    
		} // Proposals.
		
	    } // Users.
	    
	} // Tags.
	
	mylog(sweepCount, "CONTENTION "+count);
	
	logger.log(1, "Scheduler",
		   "Scheduling completed:"+
		   "\nAvailable Groups: "+count+	      
		   "\n   Best Group: "+
		   (best.getAllowed() ? best.toString() : "NONE_ALLOWED")+	
		   "\n Process Time: "+procTime+" millis.");
	
	System.err.println("Background list..");
	int bb = background.size();
	Iterator ib = background.iterator();
	while (ib.hasNext()) {
	    ScheduleDescriptor sd = (ScheduleDescriptor)ib.next();
	    System.err.println("BACKGROUND: "+sd);
	    if ((sd != null) && (sd.getGroup() != null)) {
		StringBuffer buffer = new StringBuffer();
		Iterator is = sd.listMetricKeys();
		while (is.hasNext()) {
		    String key = (String)is.next();
		    double d   = sd.getMetric(key);
		    buffer.append(key+"="+d);
		    if (is.hasNext())
			buffer.append(", ");
		}
		mytest(sweepCount, "BACKGROUND", sd.getGroup().getFullPath(), 
		       "RANK="+bb+
		       " SCORE="+sd.getScore()+
		       " EXEC="+sd.getNominalTime()+
		       " METRICS=["+buffer.toString()+"]"); 
	    }
	    bb--;	 
	    
	}
	
	System.err.println("Candidate list..");
	int cc = candidates.size();
	Iterator ic = candidates.iterator();
	while (ic.hasNext()) {
	    ScheduleDescriptor sd = (ScheduleDescriptor)ic.next();
	    Group cgroup = sd.getGroup();
	    double totalCost = -1.0;
	    if ((chargeModel != null) && (cgroup != null)) {
		ExecutionCosting costing = chargeModel.calculateCost(cgroup, now);
		totalCost = costing.getTotal();
		sd.setExecutionCosting(costing);
	    }

	    System.err.println("CAND: ["+cc+"] : "+sd);	    
	    if ((sd != null) && (sd.getGroup() != null)) {
		StringBuffer buffer = new StringBuffer();
		Iterator is = sd.listMetricKeys();
		while (is.hasNext()) {
		    String key = (String)is.next();
		    double d   = sd.getMetric(key);
		    buffer.append(key+"="+d);
		    if (is.hasNext())
			buffer.append(", ");
		}
		mytest(sweepCount, "CANDIDATE", sd.getGroup().getFullPath(), 
		       "RANK="+cc+
		       " SCORE="+sd.getScore()+
		       " EXEC="+sd.getNominalTime()+
		       " COST="+totalCost+
		       " METRICS=["+buffer.toString()+"]"); 
	    }
	    cc--;
	    
	}
	
	// ### XXX Check down the list, if the first candidate is an Eric - invoke the EricPlugin.
	// ### XXX in a seperate thread as it takes a few seconds to run.
	// ### XXX Skip any other Erics - whats left is best group or none.

	// If no flex or mon group we use the nfg.

	long newnow = System.currentTimeMillis();
	if (dsm != null) {
	    try {
		dsm.notifyListenersRunCompleted(newnow, candidates);
	    } catch (Exception e) {
		e.printStackTrace();
	    }
	}
	if (best != null) {
	    
	    Group  bestGroup = best.getGroup();
	    if (bestGroup != null) {
		mytest(sweepCount, "SELECT_CANDIDATE", bestGroup.getFullPath(), "");

		logger.log(1, "Scheduler", "Best scoring group is: "+bestGroup.getFullPath());
		return best;
		
	    } else {
		
		logger.log(1, "Scheduler", "There were no flexible or monitoring groups available");
		
		if (nfg != null) {	
		    logger.log(1, "Scheduler", "Checking executability of next fixed group: "+nfg.getFullPath());
		    boolean cando = Scheduling.checkValid(nfg);
		    if (cando) {
			long idle = +nfg.getFixedTime() - now;
			if (idle < maxIdle) {
			    mytest(sweepCount, "SELECT_FIXED", nfg.getFullPath()," After: "+(idle/1000)+" S");
			    
			    logger.log(1, "Scheduler", "Will do fixed group idle time = "+(idle/1000)+" secs");
			    ScheduleDescriptor fixed = new ScheduleDescriptor(nfg, 1.0, (long)(executionTimeVetoFactor*nfg.getNominalExecutionTime()));
			    return fixed;
			} else {
			    logger.log(1, "Scheduler", "Idle time until next fixed group is too long: "+
				       (idle/1000)+" > "+(maxIdle/1000)+" secs");
			    mylog(sweepCount, "SELECT_NONE");
			}
		    } else {
			mylog(sweepCount, "SELECT_NONE");
		    }
		}
		mylog(sweepCount, "SELECT_NONE");
	    }
	} 

	return best;
	
    } // - getBestGroup().
    
    /** Schedule a FlexibleGroup.*/    
    private ScheduleDescriptor doScheduleGroup(Group group, Proposal proposal, Tag tag) {

	logger.log(3, "Scoring Group: "+group.getFullPath());

        // will we skew results if this group fails horridly ?
        boolean skew = (Math.random() < skewbias);
	
	start = System.currentTimeMillis();

	long now = System.currentTimeMillis();

	double  time   = 0.0;
	
	double   score  = 0.0;
	double   score1 = 0.0;
	
	boolean allow  = true; 
	boolean allow1 = true; 
	long latestCompletionTime = 2000000000000L;

	double srcAz  = 0.0;
	double srcAlt = 0.0;
	
	// Create an initial ScheduleDescriptor
	ScheduleDescriptor descriptor =  
	    new ScheduleDescriptor(group, score, (long)(executionTimeVetoFactor*group.getNominalExecutionTime()));
	
	// ---------------------------------
	// Do any Group level vetoing first.
	// ---------------------------------

	// ----------------------------------------------------------------
	// 1. FixedGroup time veto (OR other RCS provided time-constraint).
	// ----------------------------------------------------------------
	allow1 = ((executionTimeVetoFactor*group.getNominalExecutionTime() + now + Scheduling.getDeltaTime() < 
		   (Scheduling.getNextFGroupTime()-fixedGroupBufferTime)) ||
		  (now > Scheduling.getNextFGroupTime()-fixedGroupMinGapTime));
	
	// Note we need to be past the NFG or NOT too close to a future NFG to start anything
	
	logger.log(2,"..Allow:   EXEC: "+allow1);
	allow &= allow1; 
	if (!allow) {
	    long ttfg = Scheduling.getNextFGroupTime() -  now;
	    return fail(descriptor, 
			"FIXED_DUE: "+(ttfg/1000.0)+" Secs.");} 
	
	// ----------------------------
	// 2. Excessive slew time veto. 
	// ----------------------------
	//	WeightingParameters slewParams = Scheduling.getSlewFnParams();
	// Group calculates its slewtime here...
	//allow1 = (slewParams.inRange(group.getSlewTime()));
	// long slewTime = group.getSlewTime(telescopeAzimuth, telescopeAltitude);
	//allow1 = (slewParams.inRange(slewTime)));
	//logger.log(2,"..Allow:   SLEW: "+allow1);
	//allow &= allow1;
	//if (!allow) {return fail(descriptor, "SLEW: "+(group.getSlewTime()/1000.0));}
	
	// snf REMOVED 2006-mar-16 due to uncertainty in slew time calculation.

	// -----------------------------
	// 3. Expiry date exceeded veto.
	// -----------------------------
	allow1 = (group.getExpiryDate() > Scheduling.getNow());
	logger.log(2,"..Allow: EXPIRY: "+allow1);
	allow &= allow1;
	// || skew;
	if (!allow) {return fail(descriptor, "EXPIRED");}
 
	// ---------------------
	// 4. Already Done veto.  
	// ---------------------
	allow1 = (!group.isDone());
	logger.log(2,"..Allow:   DONE: "+allow1);
	allow &= allow1;
	// || skew;
	if (!allow) {return fail(descriptor, "ALREADY_DONE");} 
	
	// ---------------------------------------------------
	// 5. Lunar condition  veto ( moon is up or will be and target < lunar distance).
	//    (only applies to DARK Groups)
	// ---------------------------------------------------
	// TRANSFERRED TO OBS NOW.....
	int lunar = group.getMinimumLunar();


	// --------------------------------------------------------------------------------
	// 6. Atmospheric Seeing veto. (should do probability estimate over expected time).
	// --------------------------------------------------------------------------------
	// TRANSFERRED TO OBS NOW.....
	int seeing = group.getMinimumSeeing();

	
	// --------------------------------------------------------------------------------
	// 6a. Photometricity veto. (should do probability estimate over expected time).
	// --------------------------------------------------------------------------------
	logger.log(2, "Group required photom: "+
		   (group.getPhotometric() ? "PHOTOM" : "NON_PHOTOM")+
		   " actual: "+
		   (currentPhotom ? "PHOTOM" : "SPECTRO"));
	
	if (group.getPhotometric() && ! currentPhotom)
	    allow1 = false;
	else
	    allow1 = true;
	logger.log(2, "..Allow:  PHOTOMETRIC: "+allow1);
	allow &= allow1;
	if (!allow) {
	    return fail(descriptor, 
			"PHOTOM: Current="+(currentPhotom ? "PHOTOM" : "NON_PHOTOM")+
			", Required="+(group.getPhotometric() ? "PHOTOM" : "SPECTRO"));
	} 
	

	// ------------------------------------------------------------------------
	// 7. If group uses min-conditions (L,S) and will take time T and time-left 
	//    under conditons (L,S) in Proposal < T -> veto.	
	// ------------------------------------------------------------------------
	//int lunar = group.getMinimumLunar();
	//	int seeing= group.getMinimumSeeing();
	//double alloc   = (double)proposal.getAllocatedFraction(lunar, seeing) * (double)proposal.getAllocatedTime();
	//double used    = (double)proposal.getTimeUsed(lunar, seeing); 	
	//double nominal = (double)group.getNominalExecutionTime();

	//allow1 = (3600000.0*(alloc - used) > nominal);
	//logger.log(2, "..Allow:  COND TIME ALLOC: "+allow1);
	//allow &= allow1;
	//if (!allow) {
	//  return fail(descriptor, "COND_TIME_ALLOC: "+
	//	"Alloc["+Group.toLunarString(lunar)+", "+Group.toSeeingString(seeing)+"]="+(3600.0*alloc)+" secs"+
	//	", Used="+(3600.0*used)+" secs"+
	//	", Nominal="+(nominal/1000.0)+" secs");
	//}
	
	// ------------------------------------------------------------------------
	// 8. Check if enough time to actually do the Group before a constraint.
	//    Note: a) Each Obs currently checks its target will be visible
	//             over the full (calculated) execution period.
	//          b) The Group may start at any time between NOW and 
	//             NOW + DeltaTime.
	//          c) The Group should end at startTime + maximalExecTime.
	//          d) These must be less than any constraint.
	// ------------------------------------------------------------------------
	allow1 = (executionTimeVetoFactor*group.getNominalExecutionTime() + Scheduling.getNow() +
		  Scheduling.getDeltaTime() < Scheduling.getNextTimeConstraint());
	
	logger.log(2,"..Allow:   CONSTRAINT: "+allow1);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "RCS_TIME_CONSTRAINT");} 

	// -------------------------------------------------------------------------------
	// 8a. Add in the sunrise and end observing  constraint here......
	// -------------------------------------------------------------------------------
	double maxexec = executionTimeVetoFactor*group.getNominalExecutionTime();	
	allow1 = ((long)maxexec + Scheduling.getDeltaTime() < ttsunrise - sunriseOffset);
	
	logger.log(2,"..Allow:   SUNRISE: "+allow1);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "TT_END_OBS: Expecting observing to complete in "+((ttsunrise-sunriseOffset)/1000)+" secs"+
				 " Require upto "+(maxexec/1000)+" secs to execute");} 
		

	allow1 = (maxexec + Scheduling.getDeltaTime() < ttday);	
	logger.log(2,"..Allow:   SUNRISE: "+allow1);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "TT_DAYLIGHT: Expecting sky too bright in "+((ttday)/1000)+" secs"+
				 " Require upto "+(maxexec/1000)+" secs to execute");} 
		
	// -------------------------------------------------------------------------------
	// 9. Check for twilight usage.
	// -------------------------------------------------------------------------------
	Position sun = Scheduling.getSolarPosition();
	double sunelev = sun.getAltitude();

	String strNight = "UNKNOWN";
	if (sunelev < nightElevation)
	    strNight = "NIGHT";
	else if
	    (sunelev < darkTwilightElevation)
	    strNight = "DARK_TWILIGHT";
	else if
	    (sunelev < brightTwilightElevation)
	    strNight = "BRIGHT_TWILIGHT";
	else
	    strNight = "BRIGHT";
	
	switch (group.getTwilightUsageMode()) {
	case Group.SKY_ANY:	  
	    // we allow whatever the sun elev.	    
	    allow1 = true;	  
	    break;
	case Group.SKY_BRIGHT_TWILIGHT:
	    // allow only if sun elev below brightTwilightAngle
	    allow1 = (sunelev < brightTwilightElevation);
	    break;
	case Group.SKY_DARK_TWILIGHT:
	     // allow only if sun elev below darkTwilightAngle
	    allow1 = (sunelev < darkTwilightElevation);
	    break;
	case Group.SKY_NIGHT:
	    // allow only if sun elev below nightAngle
	    allow1 = (sunelev < nightElevation);
	}
	allow &= allow1;
	if (!allow) return fail(descriptor,
				"TWILIGHT: Group requires: "+Group.toTwilightString(group.getTwilightUsageMode())+
				" Actual: "+strNight);
	
	
	// --------------------------------------
	// Apply Group's contribution to scoring.
	// --------------------------------------

	// ----------------------------    
	// 0. Score Observations first. 
	// ----------------------------   

	// Iterate over all observations.
	Iterator it = group.listAllObservations();
	logger.log(3,"Scoring observations:");
	logger.log(3,"--------------------");
	int numObs = 0;
	
	// Calculate the real slew time for this Group.
	// Note. We don't try to optimize order here.
	groupSlewTime = 0L;
	double currAz  = telescopeAzimuth;
	double currAlt = telescopeAltitude;
	
	while (it.hasNext()) {	 
	    numObs++;
	    Observation observation = (Observation)it.next(); 
	    
	    // Work out the total slew time for this Group.
	    // from current telpos to each obs.src in turn.
	    Source source = observation.getSource();
	    if (source != null) {
		Position target = source.getPosition();
		srcAz  = target.getAzimuth();
		srcAlt = target.getAltitude();
		groupSlewTime += calculateSlewTime(currAz,      currAlt, 
						   srcAz,       srcAlt, 
						   azRate,      altRate, 
						   telescopeAzNegLimit,  
						   telescopeAzPosLimit);
		// Reset position after internal slew.
		currAz  = srcAz;
		currAlt = srcAlt;
	    }

	    logger.log(3, "Calculated Group slew (inc) from current position: "+groupSlewTime+" secs");
	    
	    logger.log(3,"Vetoing observation...");
	    // AND the Observation veto functions. ## WATCH THIS !!
	    allow = allow & (doAllowObservation(descriptor,
						observation, 
						executionTimeVetoFactor*group.getNominalExecutionTime() + executionTimeFixedFactor, 
						latestCompletionTime,
						lunar,
						seeing));
	    if (!allow) {return fail(descriptor, "OBS_FAIL: "+descriptor.getFailureReason());} 
	    
	    // Check the Observation's completion time - Set if its less than current..
	    // if (observation.getLatestCompletionTime() < latestCompletionTime) {
	    //latestCompletionTime = observation.getLatestCompletionTime();
	    //}

	    // obs needs total exectime to find out if the source will remain
	    // above the dome limits for the full period ..
	    logger.log(3,"Scoring observation...");
	    score1 += doScoreObservation(descriptor, observation, group);	   	    
	} 
	if (numObs == 0)
	    return fail(descriptor, "NO_OBS_IN_GROUP");
	
	score = score + (score1/numObs);
	logger.log(3,"After all obo scores ALLOW: "+allow);
	logger.log(3,"After all obo scores SCORE: "+score);

	// Scale the observation metrics to num obs as they are too BIG.
	//double hs = descriptor.getMetric("HEIGHT");
	//descriptor.addMetric("HEIGHT", hs/numObs);

	double ts = descriptor.getMetric("TRANSIT");
	descriptor.addMetric("TRANSIT", ts/numObs);
	
	double sms = descriptor.getMetric("SEEING_MATCHING");
	descriptor.addMetric("SEEING_MATCHING", sms/numObs);

	// ----------------------------------------------------------------------------
	// 1. Slew time function. (only use the internal slewing time between sources).	
	// ----------------------------------------------------------------------------
	//WeightingParameters slewParams = Scheduling.getSlewFnParams();	
	//score1 = slewParams.evaluate((double)groupSlewTime);
	//logger.log(2,"..Scores:  SLEW: "+score1);
	//score += score1;
	//descriptor.addMetric("SLEW", score1);

	// --------------------------------------------------------------
	// 4. Distribution of time in condition blocks between Proposals. 
	// --------------------------------------------------------------

	double totalTime     = (double)proposal.getAllocatedTime();
	double allocFraction = 0.0;
	double usedTime      = 0.0;
	if (seeing >= 0) {
	    allocFraction = (double)proposal.getAllocatedFraction(lunar, seeing);
	    usedTime      = (double)proposal.getTimeUsed(lunar, seeing);
	}

	// Test for div by zero and skip if not able to do - 
	// at worst they may loose some extra score here.
	if ((allocFraction * totalTime) > SMALL_NUMBER) {
	    double usedFraction =  usedTime / (allocFraction* totalTime);

	    // The scale factor determines how fast the fn drops off.
	    double ufs = (1.0 - usedFraction);
	    score1 = wgtAlloc / ( 1.0 + scaleAlloc*ufs*ufs);
	    
	    logger.log(1,"Time used fraction: "+usedFraction+" -> Score Component: PROP_COND_MATCHING_TIME_ALLOC: "+score1);
	    score += score1; 
	    descriptor.addMetric("ALLOC_FRAC", score1);
	}

	
	// --------------------------------------------------------------
	// 4b. Proposal completion. 
	// --------------------------------------------------------------

	double allocTime     = (double)proposal.getAllocatedTime();
	double compTime      = (double)proposal.getUsedTime();
	double compFrac      = allocTime/usedTime;

	// Test for div by zero and skip if not able to do - 
	// at worst they may loose some extra score here.
	if (allocTime > SMALL_NUMBER) {
	    double compFraction =  compTime / allocTime;

	    // The scale factor determines how fast the fn climbs.
	    // climbs to 1 at 1 then drops again (shouldnt matter as > 1 is vetoed)
	    double ufs = (1.0 - compFraction);
	    score1 = wgtComp / ( 1.0 + scaleComp*ufs*ufs);
	    logger.log(1,"Completion fraction: "+compFraction+" -> Score Component: PROP_COMPLETION: "+score1);
	    score += score1; 
	    descriptor.addMetric("COMP_FRAC", score1);
	}

	// -------------------------------------------------
	// 4.a Proposals in TAG Priority.
	// -------------------------------------------------
	// Work out the normalized score over all proposals in their TAG.
	// These have been worked out by Oss at load and stored in
	// a cache either in it or in here.
	// ###double tagNorm = Scheduling.getTagPriorityNormalizationFactor(myTag.getName());
	// ###score1 = proposal.getPriority()/tagNorm;


	// b) Using assigned group priorities.
	switch (group.getPriority()) {
	case -2:
	    // Make it very low so a normal never looses out.	    
	    score1 = -10;
	    break;
	case -1:
	    // For now we use p=3 which is High.
	    score1 = wgtPriority*3.0;	   
	    break;
	default:
	    score1 = wgtPriority*(double)group.getPriority();
	    break;
	}
	score += score1;
	descriptor.addMetric("PRIORITY", score1);
	descriptor.addMetric("PV", (double)group.getPriority());

	// 6. Condition matching factor. [actual - requested] for lunar and seeing.
	
	int requestedLunar = group.getMinimumLunar();
	int actualLunar    = Group.BRIGHT;
	if (moon.isRisen() || (moon.getDownTimeMillis() < executionTimeVetoFactor*group.getNominalExecutionTime())) {
	    actualLunar = Group.BRIGHT;
	    // NOTE: Dark obs can be done in bright time if the moon is faraway !
	    //       just fudge this back to actual is DARK for now which is what we will do anyway.
	    if (requestedLunar == Group.DARK)
		actualLunar = Group.DARK;
	} else {
	    actualLunar = Group.DARK;
	}

	int requestedSeeing = group.getMinimumSeeing();
	int actualSeeing    = currentSeeing;
	
	// is this correct ?	

	score1 = wgtSeeing/(1.0 + (actualSeeing - requestedSeeing));
	logger.log(2,"..Scores: SEEING_MATCHING: "+score1);
	score +=  score1;
	descriptor.addMetric("SEEING_MATCHING", score1);

	score1 = wgtLunar/(1.0 + (actualLunar- requestedLunar));
	logger.log(2,"..Scores: DARKNESS_MATCHING: "+score1);
	score +=  score1;
	descriptor.addMetric("DARKNESS_MATCHING",score1);
	

	// -------------------------------------------------
	// 5. Fairness function. ( NOTE: We MULTIPLY  here).
	// -------------------------------------------------
	// Cant trust the values setup in the TAGS at the mo., these also need to be semester values
	// in the proposals as well - big job!
	//double targetTime       = tag.getTargetFraction(lunar, seeing) * tag.getTotalAllocatedTime();
	//double fairnessFraction = tag.getAchievedTime(lunar, seeing) / targetTime);

	double tagUsedTime   = (double)tag.getTotalAchievedTime();
	double tagTargetTime = (double)tag.getTotalAllocatedTime();

	// should be <= 1.0 but can be more...reduction tends to 0 as ff >> 1
	double ff = tagUsedTime/tagTargetTime; 

	// The scale factor (2.0) roughly determines how long the fairnesses will
	// require to even out between Tags.
	if (Double.isInfinite(ff)) {
	    logger.log(2,"Tag fairness fraction is : INF - cannot use so assuming 0.5");
	    ff = 0.5;
	} else if
	    (Double.isNaN(ff)) {	    
	    logger.log(2,"Tag fairness fraction is : NAN - cannot use so assuming 0.5");
	    ff = 0.5;
	} 
	double scf = ff/scaleFairness;	
	score1 = 1.0 / ( 1.0 + scf*scf);
	logger.log(2,"Tag fairness fraction: "+ff);
	//logger.log(2,"FAIRNESS multiplier: "+score1);
	//score *= score1;
	descriptor.addMetric("FAIRNESS_MULT",score1);
	
	logger.log(2, "*Finally  SCORE: "+score);
		
	// Set the descriptor up with relevant information.
	descriptor.setScore(score);
	descriptor.setAllowed(allow);
	descriptor.setNominalTime((long)group.getNominalExecutionTime());
	// ######TBD Set this from the earliest of the observations' longest times.
	descriptor.setLatestTime(latestCompletionTime);
	
	//System.err.println("OK Group:        "+group.getFullPath()+
	//	   "\n\tMax:       "+group.getMaximalExecutionTime()+
	//	   "\n\tNominal:   "+group.getNominalExecutionTime()+
	//	   "\n\t**Latest:  "+sdf.format(new Date(latestCompletionTime))+"**");
	
	procTime += System.currentTimeMillis() - start;
	return descriptor;		
    }

    
    /** Schedule a FlexibleGroup.*/
    private ScheduleDescriptor doScheduleFlexibleGroup(Group group, Proposal proposal, Tag tag) {
	
	ScheduleDescriptor descriptor = doScheduleGroup((Group)group, proposal, tag);
	
	long now = System.currentTimeMillis();

	// Scoring.
	double score1  = 0.0;
	double score   = descriptor.getScore();
	
	boolean allow1 = true;
	boolean allow  = descriptor.getAllowed();	
	if (!allow) {
	    return fail(descriptor, "FLEXIBLE_GENERIC_GROUP_FAILED/"+descriptor.getFailureReason());
	}
	
	long startDate = group.getStartingDate();
	allow1 = (startDate <= now);
	allow &= allow1;
	if (!allow) {
	    return fail(descriptor, "FLEXIBLE_NOT_STARTED");
	}

	// ---------------------     
	// 2. Lateness function. 
	// ---------------------        
	long expires = group.getExpiryDate();
	long entered = group.getDoneDate();


	double lateness = (double)(expires - now)/(double)(expires - entered);
	score1   = (lateness < 1.0 ? lateness : 1.0);
	score += wgtLateness*score1;
	descriptor.addMetric("LATENESS", score1);
		
	return descriptor;
	
    }
    

    
    /** Schedule a MonitorGroup.*/
    private ScheduleDescriptor doScheduleMonitorGroup(MonitorGroup group, Proposal proposal, Tag tag) {
	// Group.schedule() may return a null indicating that this group cannot
	// be scheduled for some reason - we just quit then
	
	ScheduleDescriptor descriptor = doScheduleGroup((Group)group, proposal, tag);
	start = System.currentTimeMillis();
	// Scoring.
	double score1  = 0.0;
	double score   = descriptor.getScore();
	
	boolean allow1 = true;
	boolean allow  = descriptor.getAllowed();	
	if (!allow) {return fail(descriptor, "MONITOR_GENERIC_GROUP_FAILED/"+descriptor.getFailureReason());} 
	
	// Vetos.
	
	// -----------------------------------------------
	// 1. Outside of float fraction of monitor period.
	//    or before the official start date.
	// -----------------------------------------------
	long   now           = System.currentTimeMillis();

	long   startDate     = group.getStartDate();
	long   period        = group.getPeriod();
	float  floatFraction = group.getFloatFraction();

	double fPeriod = (double)( now - startDate ) / (double)period;
	double iPeriod = Math.rint(fPeriod);

	allow1 = (startDate <= now);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "MONITORING_NOT_STARTED");} 
		   
	long endDate = group.getEndDate();
        allow1 = (endDate > now);
        allow &= allow1;
        if (!allow) {return fail(descriptor, "MONITORING_HAS_ENDED");}

	long startFloat = startDate + 
	    (long)((iPeriod - (double)floatFraction / 2.0)* (double)period);
	long endFloat   = startDate + 
	    (long)((iPeriod + (double)floatFraction / 2.0)* (double)period);
	
	allow1 = ((startFloat <= now) && (endFloat >= now));
	 	
	// Work out how many periods since last scheduled.
	//double missedPeriods = (double)((now - group.getLastScheduled()) / period);

	// No of missed periods is either: 
	// (now - lastCompleted) : if it has ever been completed
	// (now - startDate)     : if it has never been done.    

	double missedPeriods = 0.0;
	
	if (group.getLastCompleted() < startDate)
	    missedPeriods = (double)(now - startDate) / (double)period;
	else
	    missedPeriods = (double)(now - group.getLastCompleted()) / (double)period;
	
	//When did we last complete.
	double fLastPeriod = (double)(group.getLastCompleted() - startDate ) / (double)period;
	double iLastPeriod = Math.rint(fLastPeriod);
	
	logger.log(1,"MonitorGroup"+group.getFullPath()+
		   "\n\tStart date is:    "+sdf.format(new Date(startDate))+
		   "\n\tPeriod:           "+(group.getPeriod()/1000.0)+" Secs."+
		   "\n\tFraction:      +/-"+((int)(50.0f*floatFraction))+" %"+			  
		   "\n\tThis window ("+iPeriod+") runs:"+
		   "\n\t  From:           "+sdf.format(new Date(startFloat))+
		   "\n\t  To:             "+sdf.format(new Date(endFloat))+ 
		   "\n\tLast Scheduled:   "+sdf.format(new Date(group.getLastScheduled()))+
		   "\n\tLast Completed:   "+sdf.format(new Date(group.getLastCompleted()))+
		   "\n\t  in Period#      "+iLastPeriod+			
		   "\n\tLast noted period:"+group.getLastPeriod()+
		   "\n\t          IP-LP:  "+(iPeriod-iLastPeriod)+      
		   "\n\tWindow OK ?     "+(allow1 ? "YES" : "NO")+
		   "\n\tMissed Periods:   "+missedPeriods);

	allow &= allow1;
	if (!allow) {return fail(descriptor, "MONITOR_OUTSIDE_FLOAT_PERIOD");} 
	
	// Fail if we've been completed this window.
	allow1 = (iPeriod - iLastPeriod >= 1);

	allow &= allow1;
	if (!allow) {return fail(descriptor, "MONITOR_ALREADY_DONE_THIS_WINDOW");}
	
	// ----------------------------------------
	// 1. Missed periods - increment each time.
	// ----------------------------------------
// 	WeightingParameters missedPeriodParams = Scheduling.getMissedPeriodFnParams();		
// 	score1 = missedPeriodParams.evaluate(missedPeriods);

	double missedIntervals = Math.min((double)missedPeriods, 10.0);
	score1 = wgtMissedTime*missedIntervals;
	score += score1;

	logger.log(3,"..Scores:  MISSED_PERIOD: "+score1);
	score += score1;
	descriptor.addMetric("MISSED_PERIOD", score1);

	descriptor.setScore(score);
	descriptor.setAllowed(true);
	System.err.println("OK: Monitor group: "+group.getFullPath());
	procTime += System.currentTimeMillis() - start;
	return descriptor;

    }



    /** Schedule a RepeatableGroup.*/
    private ScheduleDescriptor doScheduleRepeatableGroup(RepeatableGroup group, Proposal proposal, Tag tag) {
	// Group.schedule() may return a null indicating that this group cannot
	// be scheduled for some reason - we just quit then
	ScheduleDescriptor descriptor = doScheduleGroup((Group)group, proposal, tag);
	start = System.currentTimeMillis();

	// Scoring.
	double score1  = 0.0;
	double score   = descriptor.getScore();
	
	boolean allow1 = true;
	boolean allow  = descriptor.getAllowed();	
	if (!allow) {return fail(descriptor, "REPEATABLE_GENERIC_GROUP_FAILED/"+descriptor.getFailureReason());} 

	// Vetos.
	
	// -----------------------------------------------
	// 1. Outside of observing period.	
	// -----------------------------------------------
	long now       = System.currentTimeMillis();

	long startDate = group.getStartDate();
	long endDate   = group.getEndDate();

	allow1 = (startDate <= now);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "REPEATABLE_PERIOD_NOT_STARTED");} 
		   
	allow1 = (endDate > now);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "REPEATABLE_PAST_END_PERIOD");} 
	
	// -----------------------------------------------
	// 2. Too soon after last execution.
	// -----------------------------------------------
	
	long lastCompleted = group.getLastCompleted();
	logger.log(1,"RepeatGroup "+group.getFullPath()+
		   ", Last completed: "+sdf.format(new Date(lastCompleted))+
		   ", "+((now - lastCompleted)/1000)+" S ago");
	
	allow1 = ((now - lastCompleted) > group.getMinimumInterval());
	allow &= allow1;
	if (!allow) {return fail(descriptor, "REPEATABLE_TOO_SOON_AFTER_LAST_EXEC");}
	
	// -----------------------------------------------
	// 3. Exceeded repeat count.
	// -----------------------------------------------
	
	int repeats = group.getCountCompleted();
	allow1 = (repeats <= group.getMaximumRepeats());
	allow &= allow1;
	if (!allow) {return fail(descriptor, "REPEATABLE_TOO_MANY_REPEATS");}
	
	// score number_of_missed_intervals/interval_size BUT not more than 10 of these...
	long minInterval = group.getMinimumInterval();
	double missedIntervals = Math.min((double)(now - lastCompleted), 10.0*(double)minInterval);
	score1 = wgtMissedTime*missedIntervals/(double)minInterval;
	logger.log(3,"..Scores:  MISSED_TIME: "+score1);
	score += score1;
	descriptor.addMetric("MISSED_INTERVAL", score1);

	descriptor.setScore(score);
	descriptor.setAllowed(true);
	System.err.println("OK: Repeatable group: "+group.getFullPath());
	procTime += System.currentTimeMillis() - start;
	return descriptor;
	
    }
    
    /** Schedule an EphemerisGroup. (v/similar to MonitorGroup.......TBD)*/
    private ScheduleDescriptor doScheduleEphemerisGroup(EphemerisGroup group, Proposal proposal, Tag tag) {

	// Group.schedule() may return a null indicating that this group cannot
	// be scheduled for some reason - we just quit then
	ScheduleDescriptor descriptor = doScheduleGroup((Group)group, proposal, tag);
	start = System.currentTimeMillis();

	// Scoring.
	double score1  = 0.0;
	double score   = descriptor.getScore();
	
	boolean allow1 = true;
	boolean allow  = descriptor.getAllowed();	
	if (!allow) {return fail(descriptor, "EPHEM_GENERIC_GROUP_FAILED/"+descriptor.getFailureReason());} 
	
	// Vetos.
	
	// -----------------------------------------------
	// 1. Outside of float fraction of ephem period.
	//    or before the official start date.
	// -----------------------------------------------
	long   now           = System.currentTimeMillis();

	long   startDate     = group.getStart();
	long   endDate       = group.getEnd();
	long   period        = group.getPeriod();
	double phase         = (double)group.getPhase();
	double slopFraction  = (double)group.getSlopPhase();
	
	double fperiod = Math.floor((now - startDate)/period);
	int iperiod = (int)fperiod;

	allow1 = (startDate <= now);
	allow &= allow1;
	if (!allow) {return fail(descriptor, "EPHEM_NOT_STARTED");} 
		   
        allow1 = (endDate > now);
        allow &= allow1;
        if (!allow) {return fail(descriptor, "EPHEM_HAS_ENDED");}

	long startWindow = startDate + (long)((fperiod + phase - slopFraction/2.0) * period);
	long endWindow   = startDate + (long)((fperiod + phase + slopFraction/2.0) * period);
	
	allow1 = ((startWindow <= now) && (endWindow >= now));
	 	
	double missedPeriods = (double)((now - startDate) / (endDate - startDate));
			

	logger.log(1,"EphemGroup "+group.getFullPath()+
		   "\n\tStart date is:    "+sdf.format(new Date(startDate))+
		   "\n\tPeriod:           "+(group.getPeriod()/1000.0)+" secs."+
		   "\n\tPhase:            "+(100.0*group.getPhase())+" %"+
		   "\n\tFraction:      +/-"+(50.0f*slopFraction)+" %"+	
		   "\n\tWindow size:      "+((slopFraction*period)/1000.0)+" secs."+
		   "\n\t Window ("+iperiod+") runs:"+
		   "\n\t  From:           "+sdf.format(new Date(startWindow))+
		   "\n\t  To:             "+sdf.format(new Date(endWindow))+ 		  
		   "\n\tWindow OK ?     "+(allow1 ? "YES" : "NO")+
		   "\n\tMissed Periods:   "+missedPeriods);

	allow &= allow1;
	if (!allow) {return fail(descriptor, "EPHEM_OUTSIDE_WINDOW");} 
	
	// Work out how many periods we might have missed - i.e. this should give
	// a score which increases from 0 at startDate to 1 at endDate.


	score1 = missedPeriods*wgtEphem;
	descriptor.addMetric("MISSED_CYCLES", score1);

	logger.log(3,"..Scores:  MISSED_CYCLES: "+score1);
	score += score1;

	descriptor.setScore(score);
	descriptor.setAllowed(true);
	System.err.println("OK: Ephem group: "+group.getFullPath());

	procTime += System.currentTimeMillis() - start;
	return descriptor;
	
    }

    /** Score an Observation.*/
    private double doScoreObservation(ScheduleDescriptor descriptor, Observation obs, Group group) {
	
	// -------------------------------------------------------------------------------
	// Implement Observation Level efficiency weighting functions.
	// -------------------------------------------------------------------------------
	double   score  = 0.0;
	Position target = obs.getSource().getPosition();
	
	if (target.isRisen(domeLimit)) logger.log(3, "Star is ABOVE dome limit.");
	if (target.isSet(domeLimit))   logger.log(3, "Star is BELOW dome limit.");
	if (target.isRising())   logger.log(3, "Star is RISING.");
	if (target.isSetting())  logger.log(3, "Star is SETTING.");
	
	if (!target.neverRises(domeLimit)) {
	    logger.log(3, "Star rises: "+Position.toHMSString(target.getRiseTime()));
	} else {
	    logger.log(3, "Star NEVER RISES.");
	}
	
	if (!target.neverSets(domeLimit)) {
	    logger.log(3, "Star  sets: "+Position.toHMSString(target.getSetTime()));
	} else {
	    logger.log(3, "Star NEVER SETS.");
	}

	logger.log(2, "Twilight usage: "+group.getTwilightUsageMode());
	
	// Check sunrise/set we don't veto even if its up during simulation!!		
	if (sun.isRisen()) {
	    logger.log(3, "Sun is ABOVE Horizon."+
		       "Time left till sunset: "+
		       Position.toHMSString(sun.getUpTimeMillis()/13750987.08)+
		       "End of evening twilight: "+
		       Position.toHMSString(sun.getUpTimeMillis(-Math.toRadians(18.0))/13750987.08));
	}
		
		
	// -------------------------------------------------------------------------------
	// 1. Height function.   
	// -------------------------------------------------------------------------------
	//WeightingParameters heightParams = Scheduling.getHeightFnParams(); 
	
	//double ht = target.getAltitude();
	//double hs = heightParams.evaluate(ht);
	//score += hs;

	//double htscore = descriptor.getMetric("HEIGHT");
	//descriptor.addMetric("HEIGHT", htscore+hs);

	// -------------------------------------------------------------------------------
	// 2. Transit Height function.    
	// -------------------------------------------------------------------------------
	//WeightingParameters transitParams = Scheduling.getTransitFnParams(); 
	
	// Ideally we want the target's highest elev in current window so that
	// we can use height_now/max_height - this is a bugger to work out as
	// there are multiple scenarios depending on whether the window is
	// more or less than a night and whether the transit occurs in the window
	// or outside it. 

	// Check for small angles. Note: if tth is small negative
	// we get the wrong sign for th but Height will veto anyway.

	// we now use the max elev in coming night value

	double tth = Scheduling.getMetricsCacheValue(group);
	//	double tth = target.getTransitHeight();
	if (Math.abs(tth) < VERY_SMALL)
	  tth = VERY_SMALL;
	double elev = target.getAltitude();	
	double th = elev/tth;
	//double ths = transitParams.evaluate(th);

	double ths = wgtTransit*th;
	score += ths;

	double transcore = descriptor.getMetric("TRANSIT");
	descriptor.addMetric("TRANSIT", transcore+ths);

	// 3a Seeing matching using observed seeing at elevation...
	int requestedSeeing = group.getMinimumSeeing();

        double zd = 0.5*Math.PI-elev;
        double cosz = Math.cos(zd);
        double obsSeeingValue = correctedSeeing/Math.pow(cosz, 0.6);
        // now determine the band
        int obsSee = Scheduling.determineSeeingBand(obsSeeingValue);
	
	double sms = wgtSeeing/(1.0 + (obsSee - requestedSeeing));
	score += sms;

	double seescore = descriptor.getMetric("SEEING_MATCHING");
        descriptor.addMetric("SEEING_MATCHING", seescore+sms);


	// 3. Lunar angular distance. ## NOT SCORED ##
	
	// 4. Meridian limits. ## NOT SCORED ##
	
	// 5. Twilight. I,R,Z band and PUST. ## NOT SCORED ##
	logger.log(2, "..Scores: TOTAL: "+score);
	
	return score;

    }

    /** Check Observation vetos.*/
    private boolean doAllowObservation(ScheduleDescriptor descriptor,
				       Observation        obs, 
				       double             maximalExecutionTime, 
				       double             latestCompletionTime,
				       int                lunar,
				       int                seeing) {
	
	// Implement Veto functions.	
	boolean allow   = true;
	boolean allow1  = true;
		
	long now = System.currentTimeMillis();

	// Is there a Target.
	Source   source = obs.getSource();
	if (source == null) return failobs(descriptor,"NO_TARGET:");
	Position target = obs.getSource().getPosition();

	// Slot the declination limit in here.
	//if (target.getDeclination() < minDec)
	//  return failobs(descriptor,"BELOW_DEC_LIMIT");

	
	// -------------------------------------------------------------------------------
	// 1. Height veto function.   
	// -------------------------------------------------------------------------------
	//WeightingParameters heightParams = Scheduling.getHeightFnParams();          
	double ht = target.getAltitude();
	
	allow1 = (ht > domeLimit);
	logger.log(2, "..Allow:   HEIGHT: "+allow1);
	allow &= allow1;
	if (!allow) return failobs(descriptor,"HEIGHT: "+Math.toDegrees(ht)+" degs.");
	
	// -------------------------------------------------------------------------------
	// 2. Transit veto function. check against altitude HI limit.
	// -------------------------------------------------------------------------------
        double transit = target.getTransitHeight();
	double altCeiling = Math.min(telescopeAltHighLimit, altTempHighLimit);

	logger.log(2, "Checking Transit elevation: "+
		   Position.toDegrees(transit,2)+
		   " against ceiling: "+Position.toDegrees(altCeiling,2));
	
	// Only need to check if target can rise above HI limit.
	if (transit > altCeiling) {

	    double hs = target.getHA();
	    double he = target.getHA(now + (long)maximalExecutionTime);

	    //double he = hs + (Math.PI*2.0*exec/86400000.0);
	    //if (he > Math.PI*2.0)
	    //  he -= Math.PI*2.0;	

	    logger.log(2, "Determining setting time for target...");
	    double H0 = target.getSetTime(altCeiling); // HA of setting in [0, PI].
	    double H1 = 2.0*Math.PI-H0;
	    
	    logger.log(2, "Setting HA is "+Position.toDegrees(H0,2));
	    
	    boolean altOkay = false;

	    // Case A/F: Pre-rise/Post-set: Tgt not risen before H1 or set after H0
	    if (hs < H1 && he < H1 && hs > H0 && he > H0)
		altOkay = true;
	    // Case B: Riser:    Tgt rises thro H1
	    // Case C: Internal: Tgt up after H1 and set before H0
	    // Case D: External: Tgt rises before H1 and sets after H0
	    // Case E: Setter:   Tgt sets thro H0
	    	    
	    if (! altOkay)
		return failobs(descriptor,"TRANSIT: H-Obs=["+Position.toHMSString(hs)+","+ Position.toHMSString(he)+"]"+
			       " Crosses altitude ceiling "+Position.toDegrees(altCeiling,2)+
			       " with HA (H1/H0)=("+Position.toHMSString(H1)+"/"+ Position.toHMSString(H0)+")");
	    
	    logger.log(2, "..Allow:   CEILING: "+altOkay);
	
	}
	
	// -------------------------------------------------------------------------------
	// 2a1. Zenith veto function.  (we do need this!)  
	// -------------------------------------------------------------------------------

	double exec = maximalExecutionTime+Scheduling.getDeltaTime();

	double hs = target.getHA();
	double he = hs + (Math.PI*2.0*exec/86400000.0);
	if (he > Math.PI*2.0)
	    he -= Math.PI*2.0;	
	double trans = target.getTransitHeight();
	// OK
	if (trans < 0.5*Math.PI-zezAngle ||
	    trans > 0.5*Math.PI+zezAngle) {	    
	    //System.err.println("RULE 0: PASS - Never passes thro zezAngle");
	    allow1 = true;
	} else {	    
	    System.err.println("RULE 0: FAIL - Passes thro zezAngle");	    
	    double ds = Math.min(2.0*Math.PI-hs, hs);
	    double de = Math.min(2.0*Math.PI-he, he);	    
	    if (ds < zezAngle) {
		//System.err.println("RULE 1: FAIL - Starts inside zezAngle");
		allow1 = false;
	    } else {		
		System.err.println("RULE 1: PASS - Does not start inside zezAngle");		
		if (de < zezAngle) {		    
		    //System.err.println("RULE 2: FAIL - Ends inside zezAngle");
		    allow1 = false;
		} else {		    
		    System.err.println("RULE 2: PASS - Does not end inside zezAngle");		    
		    if (hs < 2.0*Math.PI-zezAngle &&
			he > zezAngle &&
			he < hs) {			
			//System.err.println("RULE 3: FAIL - Crosses over zezAngle");
			allow1 = false;
		    } else {			
			//System.err.println("RULE 3: PASS - Does not cross over zezAngle");
			allow1 = true;
		    }
		}
	    }
	}
	allow &= allow1;
	logger.log(2, "..Allow:  ZENITH: "+allow1);
	if (!allow) return failobs(descriptor,"ZENITH_CROSSING: ");

	// --------------------------------------------------------------------------------
        // 1c. Atmospheric Seeing veto. (should do probability estimate over expected time).
        // --------------------------------------------------------------------------------
	// de-correct the Rband Zenith seeing to targets elevation now

	double elev = target.getAltitude();
	double zd = 0.5*Math.PI-elev;
	double cosz = Math.cos(zd);
	double obsSeeingValue = correctedSeeing/Math.pow(cosz, 0.6);
	// now determine the band
	int obsSee = Scheduling.determineSeeingBand(obsSeeingValue);
	
	logger.log(3, 
		   "Wrapping Group required seeing at least: "+Group.toSeeingString(seeing)+
		   " Current corrected to ZD=0,R-band: "+correctedSeeing+" ("+Group.toSeeingString(currentSeeing)+")"+
		   " At target elev ("+Position.toDegrees(elev,2)+"): "+obsSeeingValue+" ("+Group.toSeeingString(obsSee)+")");

        allow1 = (seeing <= obsSee) ;
        logger.log(2, "..Allow:  SEEING: "+allow1);
        allow &= allow1;
	if (!allow1) {
	    //    return failobs(descriptor,
	    //	   "SEEING: Group required seeing at least: "+Group.toSeeingString(seeing)+
	    //	   " Current seeing at ZD=0,R-band: "+correctedSeeing+" ("+Group.toSeeingString(currentSeeing)+")"+
	    //	   " At target elev ("+Position.toDegrees(elev,2)+"): "+obsSeeingValue+" ("+Group.toSeeingString(obsSee)+")");	
	    return failobs(descriptor,
			   "SEEING: Req ["+Group.toSeeingString(seeing)+"], CSZR ="+correctedSeeing+
			   " ["+Group.toSeeingString(currentSeeing)+"], AtEl ("+Position.toDegrees(elev,2)+")= "+
			   obsSeeingValue+" ["+Group.toSeeingString(obsSee)+"]");
	}

	// -------------------------------------------------------------------------------
	// 2a. Changed the Lunar DARK/BRIGHT crierion here
	// -------------------------------------------------------------------------------

	double lunarDistance = target.getAngularDistance(moon);
	
	if (lunar == Group.DARK) {
	    logger.log(3, "Wrapping Group requires DARK CONDITIONS");
	    
	    if (moon.isRisen()) {
		//if (lunarDistance > darknessCriterionLunarDistance)  
		// allow = true;
		//else {
		allow = false;
		//return failobs(descriptor, "MOON_UP_TOO_CLOSE"); 
		return failobs(descriptor, "MOON_UP");
		//}
	    } else {
		logger.log(3,"Moon is Below Horizon. - sky is DARK");
		logger.log(3,"Time left till moonrise: "+(moon.getDownTimeMillis()/1000.0)+"secs");
		
		if (moon.getDownTimeMillis() < maximalExecutionTime) {
		    logger.log(3, "Moon will rise before group has finished"+
			       "..Allow   MOON: false");
		    
		    //if (lunarDistance > darknessCriterionLunarDistance)  
		    //allow = true;
		    //else {
		    allow = false;
		    //return failobs(descriptor, "MOON_WILL_RISE_TOO_CLOSE"); 
		    return failobs(descriptor, "MOON_WILL_RISE");
		    //}
		}
		latestCompletionTime = Scheduling.getNow()+(long)moon.getDownTimeMillis();
	    }
	}

	// -------------------------------------------------------------------------------
	// 3. Lunar angular distance veto function.
	// For now this is a sharp cutoff at (configurable) degrees.
	// Note: There is no limit if the source IS the MOON !

	// allow &= lunarParams.inRange(lunarDistance);
	//WeightingParameters lunarParams = Scheduling.getLunarDistanceFnParams();
	//double lunarDistance = Position.getAngularSeparation(target, moon);
	//allow &= 
	// -------------------------------------------------------------------------------
	boolean sourceIsMoon = false;
	if (source instanceof CatalogSource) {	    
	    sourceIsMoon = (((CatalogSource)source).getCatalogId() == CatalogSource.MOON);
	}

	//absoluteMinimumLunarDistance;
	if ( ! sourceIsMoon) {
	    // We are not looking at the moon so we need to look at the distance.
	   
	    //Group.DEFAULT_MINIMUM_LUNAR_DISTANCE
	    logger.log(2,  "Lunar angle distance: "+Position.toDegrees(lunarDistance, 3));
	    double mld = group.getMinimumLunarDistance();
	    if (mld <= 0.0 ) {
		// None specified - use default
		if (lunarDistance < absoluteMinimumLunarDistance) {
		    logger.log(2, "..Allow: LUNAR DIST: false");
		    allow = false;
		    return failobs(descriptor, "ABS_LUNAR_DIST: "+
				   Position.toDegrees(lunarDistance, 3)+" < "+
				   Position.toDegrees(absoluteMinimumLunarDistance, 3));
		}
	    } else {
		// Group has specific MLD.
		if (lunarDistance < mld) {
                    logger.log(2, "..Allow: LUNAR DIST: false");
                    allow = false;
                    return failobs(descriptor, "SPEC_LUNAR_DIST: "+
                                   Position.toDegrees(lunarDistance, 3)+" < "+
                                   Position.toDegrees(mld, 3));
                }		
	    }
	}

	// -------------------------------------------------------------------------------
	// 4. Meridian veto. THIS IS MESSY !!!!
	// -------------------------------------------------------------------------------
	long meridianDistance = (long)(target.getHA()*43200000.0/Math.PI);// in msecs
	logger.log(2, "Meridian distance: +/-"+(meridianDistance/60000)+" mins.");
	// We want the absolute distance.
	if (meridianDistance > 43200000) meridianDistance = 86400000 - meridianDistance;
	// A meridianLimit of < 1 (millisecond) MEANS (no limit specified) so always succeeds.
	allow1 = ! (group.getMeridianLimit() > 1 &&  meridianDistance > group.getMeridianLimit());
	logger.log(2, "..Allow: MERIDIAN: "+ allow1);
	allow &= allow1;
	if (!allow) return failobs(descriptor,"MERIDIAN:");

	// -------------------------------------------------------------------------------
	// 5. Instrument availability veto.
	// -------------------------------------------------------------------------------
	
	// Is there an IC.
	InstrumentConfig instrumentConfig = obs.getInstrumentConfig();
	if (instrumentConfig == null) return failobs(descriptor,"NO_INST_CONFIG:");
		
 	// See if we recognise the instrument.
  	Instrument inst = TelescopeStatus.getInstrumentFor(instrumentConfig);	
	
 	if (inst == null) {
 	    logger.log(1, "OOPS Requested Instrument for config: "+
 		       instrumentConfig.getClass().getName()+" Not known.");
 	    allow = false;
 	    return failobs(descriptor,"UNKNOWN_INST_CLASS: "+instrumentConfig.getClass().getName());
	    
 	}

	//Veto non-available instrument.
	if (inst.getStatus() == Instrument.OFFLINE) {
	    allow = false;	    
	    return failobs(descriptor,"INST_OFFLINE: "+inst.getName());
	}
	
	// Check if instrument is operational (not just online) 
	// treat UNAVAIL and WARN as OKAY for now...
	int opState = inst.getOperationalStatus();
	switch (opState) {
	case Instrument.OPERATIONAL_STATUS_OKAY:
	    break;
	case Instrument.OPERATIONAL_STATUS_WARN:
	    logger.log(1, "Scheduler",
		       "** Warning- Instrument "+inst.getName()+" op state WARN");
	    break;
	case Instrument.OPERATIONAL_STATUS_UNAVAILABLE:
	    logger.log(1, "Scheduler",
		       "** Warning- Instrument "+inst.getName()+" op state UNKNOWN");
	    break;
	case Instrument.OPERATIONAL_STATUS_FAIL:
	    allow = false;	    
	    return failobs(descriptor,"INST_NON_OPERATIONAL: "+inst.getName());	    
	default:
	    // the above are the only valid states.
	    allow = false;
	    // this is NOT the same as UNAVAILABLE which just means we dont have the data	    
	    return failobs(descriptor,"INST_OPSTATE_INVALID: "+opState+" for "+inst.getName());	   
	}
	
 	// Check the Instrument config is actually valid.
 	if (! (inst.canBeConfigured(instrumentConfig))) 
 	    return failobs(descriptor,"INVALID_INST_CONFIG: "+instrumentConfig);
	
	//--------------------------------------------------------------------------------
	// 5a. Azimuth Limits veto.
	//--------------------------------------------------------------------------------
	
	// The source azimuth will always be in [0, 360], but we need to see if (src.az-360)
	// can be within the az limits. If either (src.az) or (src.az-360) is ok then its OK.

	// NOTE: We dont check this over the full execution for now.....
	//       ....this is OK for short exposures during testing.

	System.err.println("Checking: Target: "+target);

	double azStart = target.getAzimuth();
	System.err.println("Az start "+Position.toDegrees(azStart,3));

	boolean inlimits = (((telescopeAzNegLimit <= azStart) &&
			     (azStart <= telescopeAzPosLimit)) ||
			    ((telescopeAzNegLimit <= (azStart - 2*Math.PI)) &&
			     ((azStart - 2*Math.PI) <= telescopeAzPosLimit)));
	System.err.println("Start In limits: "+inlimits);
	if (! inlimits)
	    return failobs(descriptor,"AZIMUTH_LIMITS");	
	System.err.println("Work out azimuth in "+(maximalExecutionTime/1000)+" secs from now...");

	double azEnd = target.getAzimuth((long)(now + maximalExecutionTime));
	System.err.println("Az end "+Position.toDegrees(azEnd,3));
	
	// ### This is a temp fix to work around an LT problem - be careful about deploying generally
	// ### as the BAD zone may not make any sense for proper az limits...

	// This may NOT always work - depends on quadrants...these values are [0,360] hopefully

	boolean badZone = (telescopeAzPosLimit < 2*Math.PI + telescopeAzNegLimit);
	if (badZone) {
	    double badNegLimit = telescopeAzPosLimit;
	    double badPosLimit = 2*Math.PI + telescopeAzNegLimit;
	    System.err.println("Bad zone: "+Position.toDegrees(badNegLimit,3)+", "+ Position.toDegrees(badPosLimit,3));

	    // start and end before bad zone.
	    boolean r1 = ((((azStart < badNegLimit) && (azEnd < badNegLimit))));
	    //||
	    //(((azStart - 2*Math.PI) < badNegLimit) && ((azEnd - 2*Math.PI) < badNegLimit)));
	    
	    // start and end after bad zone.
	    boolean r2 = ((((azStart > badPosLimit) && (azEnd > badPosLimit))));
	    //||
	    //(((azStart - 2*Math.PI) > badNegLimit) && ((azEnd - 2*Math.PI) > badNegLimit)));
	
	    boolean willRemainInLimits = (r1 || r2);
	
	    if (! willRemainInLimits) 
		return failobs(descriptor,"AZ_WILL_LIMIT_DURING_EXEC: "+
			       " Start_Az="+Position.toDegrees(azStart,3)+
			       ", End_Az="+Position.toDegrees(azEnd,3));
	    else 
		System.err.println("Will remain in limits");
	}
	

	//--------------------------------------------------------------------------------
	// 5b. Autoguider availability veto.
	//--------------------------------------------------------------------------------

	int agUsageMode = obs.getAutoGuiderUsageMode();

	// If AG required then we need it.
	if ((! autoguiderAvailable) && 
	    (agUsageMode == TelescopeConfig.AGMODE_MANDATORY))
	    return failobs(descriptor,"REQD_AG_NOT_AVAILABLE:");
	
	// -------------------------------------------------------------------------------
	// 6. Visible over full executionTime veto.
	// source must be above horizon for the full maximalExecutionTime + any initial deltaTime.
	// -------------------------------------------------------------------------------
	logger.log(3, "Actual Time left above dme limits: "+(target.getUpTimeMillis(domeLimit)/1000.0)+" secs"+
		   "\nMaximal Time needed for execution:   "+((maximalExecutionTime)/1000.0)+" secs"+
		   "\nInitial Start float period:  "+(Scheduling.getDeltaTime()/1000.0)+" secs");
	
	allow1 = (target.getUpTimeMillis(domeLimit) >= (maximalExecutionTime+Scheduling.getDeltaTime()) ||
		  target.neverSets(domeLimit));
	logger.log(3, "..Allow: VISIBLE_HORIZON: "+allow1);
	allow &= allow1;
	
	if (!allow1) 
	    failobs(descriptor,"VISIBLE_HORIZON: Targ alt: "+Math.toDegrees(ht)+" degs."+
		    " Time to set: "+(target.getUpTimeMillis(domeLimit)/1000.0)+" secs."+
		    " Requires:("+ ((maximalExecutionTime+Scheduling.getDeltaTime())/1000.0)+" secs).");
	
	//if (target.neverSets())
	//  System.err.println(obs.getFullPath()+" - Target does not set.");
	
	// #########################################################
	// ## We need to reset the latestCompletion here somehow. ##
	// #########################################################    
 
	// a) Check target set time against latest completion.
	if (allow) {
	    // If target will set before currently specified latest time.
	    // We need to revise the latest completion time.
	    if (Scheduling.getNow()+(long)target.getUpTimeMillis(domeLimit) < latestCompletionTime) {
		latestCompletionTime = Scheduling.getNow()+(long)target.getUpTimeMillis(domeLimit);
	    }
	}


	// -------------------------------------------------------------------------------
	// 8. RCS Rotator override veto.
	// Veto any obs where the rotator cannot be set to MOU-0 plus FLOAT.
	// -------------------------------------------------------------------------------

	allow1 = true;
	int mode = obs.getRotatorMode();
	double angle = obs.getRotatorAngle();
	switch (mode) {	 	    
	case TelescopeConfig.ROTATOR_MODE_SKY:
	    // Should test if there are different sky angles required for different obs
	    // in a group in which case it makes no sense to do this...
	    // XXX TBD
	    break;
	case TelescopeConfig.ROTATOR_MODE_MOUNT:
	    if (angle < telescopeRotNegLimit || angle > telescopeRotPosLimit) {
		failobs(descriptor,"ROTATOR OVERRIDE: Requested MOUNT angle "+
			Math.toDegrees(angle)+" degs is outside acceptable range");
		allow1 = false;
	    } 
	    break;
	case TelescopeConfig.ROTATOR_MODE_VERTICAL:
	case TelescopeConfig.ROTATOR_MODE_VFLOAT:	    
	    allow1 = ! rotatorOverridden;
	}
	if (!allow1) 
	    failobs(descriptor,"ROTATOR OVERRIDE: Requested rotator mode: "+
		    TelescopeConfig.toRotatorModeString(mode)+
		    " not compatible with overridden rotator settings");
	allow &= allow1;

	return allow;
    }

    /** Sets up a descriptor to indicate failure. 
     * @return The descriptor passed in suitably modified.
     */
    public ScheduleDescriptor fail(ScheduleDescriptor descriptor, String reason) {
	Group xx = descriptor.getGroup();
	descriptor.setGroup(null);
	descriptor.setAllowed(false);
	descriptor.setAvailableGroupCount(0);
	//logger.log(1, "Failed due to: "+reason);
	descriptor.setFailureReason(reason);
	procTime += System.currentTimeMillis() - start;
	return descriptor;
    }

    private boolean failobs(ScheduleDescriptor descriptor, String text) {
	//logger.log(1, "Scheduler", 
	//   "Group failed on OBS: "+text);
	descriptor.setFailureReason(text);
	return false;
    }

    /** Dummy implementation.*/
    public double getSchedulability(Group group, long startTest, long endTest) {
	return 0.0;   
    }

    /** Returns the slew time between 2 azimuth positions (secs).
     * A negative slew time means cannot do it.*/
    private double calculateSlewTime(double currAz,   double currAlt, 
	 			     double newAz,    double newAlt, 
				     double azRate,   double altRate,
				     double azNegLim, double azPosLim) {
	
	// Simplistic answer ignoring wraps.
	return Math.max(Math.abs((currAz - newAz)/azRate), Math.abs((currAlt - newAlt)/altRate));
	
    }

 
    /**Caclulate az slew time (check this......).*/
    public double calcRealSlewTime(double currAz,   double currAlt, 
				   double newAz,    double newAlt, 
				   double azRate,   double altRate,
				   double azNegLim, double azPosLim) {
	// Correct answer with wraps.
	// The [l] values are always positive so long as currAz is the real az
	// and not corrected for wrap.
	double ll = currAz - azNegLim;
	double lr = azPosLim - currAz;
	// The [d] values must be in [0, 2*PI] as they are diffs.
	double dl = correct(currAz - newAz);
	double dr = correct(newAz - currAz);
	int dir = 0;
	if (dl < ll) {
	    if (dr < lr) {
		if (dl < dr) {
		    dir = -1;
		} else {
		    dir = 1;
		}
	    } else {
		dir = -1;
	    }
	} else {
	    if (dr < lr) {
		dir = 1;
	    } else {
		dir = 0;
	    }
	}
	double taz = 0.0;
	if (dir == -1)
	    taz = dl/azRate;
	else if 
	    (dir == 1)
	    taz = dr/azRate;
	else if
	    (dir == 0)
	    taz = -1.0;
	return taz;
    }
    
    /** Correct az into correct quadrant.*/
    private double correct(double az) {
	if (az < 0.0)
	    return az + Math.PI*2.0;
	if (az > Math.PI*2.0)
	    return az - Math.PI*2.0;
	return az;
    }

    /** Returns the sweep counter.*/
    public int getSweepCount() { return sweepCount;}

    /** Log for sweep count.*/
    private void mylog(int sw, String text) {
	logger.log(1, "["+sw+"] "+text);
	// e.g. 2007-01-22 T 15:33:15 UTC [32] CANDIDATE (hj67B-eph) RANK=3 SD=xxxxx
    }

    /** Test log for sweep count.*/
    private void mytest(int sw, String key, String name, String text) {
	logger.log(1, "["+sw+"] "+key+" ("+name+") "+text);

	// e.g. 2007-01-22 T 15:33:15 UT [32] PROPOSAL (JL06B03) FAILED: Out of time
    } 

}
